package com.psytech.app.task.tasks_mds.service.impl;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.Query;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.log4j.Logger;
import org.hibernate.HibernateException;
import org.springframework.stereotype.Repository;

import com.psytech.app.task.tasks_mds.entity.EntityComment;
import com.psytech.app.task.tasks_mds.entity.EntityTask;
import com.psytech.app.task.tasks_mds.entity.EntityTodo;
import com.psytech.app.task.tasks_mds.entity.STMEntity;

/**
 * Couche d'accés au référentiel de données.
 * 
 * @author psychoben@gmail.com
 */
@Repository
public class Dao {

	private static final Logger LOG = Logger.getLogger(Dao.class);

	private transient String sqlVersionRequest = null;

	@PersistenceContext(type = PersistenceContextType.TRANSACTION)
	protected transient EntityManager entityManager4TASK;// NOPMD


	public EntityManager getEntityManager4TASK(){
		return entityManager4TASK;
	}

	/**
	 * Persiste un nouvel entity bean en base. La référence de l'objet est
	 * modifiée.
	 * 
	 * @param entity
	 *            l'objet à sauver
	 */
	public void create(final STMEntity entity) {
		entityManager4TASK.persist(entity);
	}

	/**
	 * Met à jour un entity bean dans le référentiel.
	 * 
	 * @param entity
	 *            l'object à mettre à jour
	 * @return l'objet mis à jour
	 */
	public Object update(final STMEntity entity) {
		LOG.debug("mise à jour d'un objet");
		return entityManager4TASK.merge(entity);
	}

	/**
	 * Permet de supprimer la référence à l'entité du référentiel. Une fois
	 * supprimé, l'objet ne pourra être récupéré.
	 * 
	 * @param entity
	 *            l'objet à supprimer définitivement du référentiel.
	 */
	public void remove(final STMEntity entity) {
		Object uid = null;
		try {
			LOG.debug("suppression d'une instance de l'object : " + entity.getClass());
			final Method getUid = entity.getClass().getMethod("getUid", (Class[]) null);
			uid = getUid.invoke(entity);
		} catch (final Throwable throwable) {
			throw new HibernateException("La méthode getId n'existe pas ou est privée", throwable);
		}
		try {
			if (uid != null) {
				final Object embeddedObjet = entityManager4TASK.find(entity.getClass(), uid);
				if (embeddedObjet != null)
					entityManager4TASK.remove(embeddedObjet);
			}
		} catch (final Throwable exception) {
			LOG.error("Erreur lors de la suppression du bean", exception);
			try {
			} catch (final Throwable e) {
				LOG.error("Rollback impossible", e);
			}
			throw new HibernateException("Impossible de supprimer le bean");
		}
	}

	/**
	 * Retrouve un objet dans le référentiel à partir de son UID.
	 * 
	 * @param entity
	 *            l'objet à retrouver dans le référentiel
	 * @return l'objet complété avec les données du référentiel, ou null sinon.
	 */
	public Object find(final STMEntity entity) {
		try {
			final Method getUid = entity.getClass().getMethod("getUid", (Class[]) null);
			return entityManager4TASK.find(entity.getClass(), getUid.invoke(entity));
		} catch (final Throwable throwable) {
			LOG.error("impossible d'executer le find", throwable);
			return null;
		}
	}

	/**
	 * Renvoi la version de la base de données utilisée comme référentiel
	 * principal.
	 * 
	 * @return un String représentant le version.
	 */
	public String getDBVersion() {
		return (String) entityManager4TASK.createNativeQuery(sqlVersionRequest).getSingleResult();
	}

	/**
	 * Définie la requête à utiliser pour récupérer la version de la base de
	 * donnée. Voir aussi {@link Dao#getDBVersion}.
	 * 
	 * @param sqlVersionRequest
	 *            la requête au format SQL. ! Implementation dependent !
	 */
	public void setSqlVersionRequest(final String sqlVersionRequest) {
		this.sqlVersionRequest = sqlVersionRequest;
	}

	/**
	 * Renvoi la liste de toutes les tâches présentes dans le référentiel.
	 */
	@SuppressWarnings("unchecked")
	public List<EntityTask> getAllTask() {
		return entityManager4TASK.createQuery("SELECT t FROM EntityTask AS t ORDER BY t.priority DESC").getResultList();
	}

	/**
	 * Renvoi la liste des tâches affectées à un utilisateur.
	 * 
	 * @param userUid
	 *            Uid de l'utilisateur pour
	 *            lequel récupérer les tâches.
	 */
	@SuppressWarnings("unchecked")
	public List<EntityTask> getAffectedTasks(final String userUid) {
		return entityManager4TASK.createQuery("select t from EntityTask as t join t.users u where u.id = ? order by t.priority DESC")
		.setParameter(1, userUid).getResultList();
	}

	/**
	 * Renvoi une liste de tâches correspondant aux critéres en entrée.
	 * 
	 * @param searchCriteria
	 *            l'entity bean
	 *            {@link com.psytech.app.task.tasks_mds.entity.EntityTask}
	 *            contenant les critéres de recherche.
	 * @param descriptionIsNull
	 *            true si la tâche recherchée doit avoir une description nulle.
	 * @param categoryIsNull
	 *            true si la tâche recherchée doit avoir une categorie nulle.
	 * @param expectedEndDateIsNull
	 *            true si la tâche recherchée doit avoir une date de fin attendue
	 *            nulle.
	 * @param expectedStartDateIsNull
	 *            true si la tâche recherchée doit avoir une date de debut
	 *            attendue nulle.
	 * @param assignedUsersIsNull
	 * 			  true si la tâche recherchée ne doit pas être assignée.
	 */
	@SuppressWarnings("unchecked")
	public List<EntityTask> findTasks(final EntityTask searchCriteria, final boolean descriptionIsNull, final boolean categoryIsNull,
			final boolean expectedEndDateIsNull, final boolean expectedStartDateIsNull, final boolean assignedUsersIsNull) {
		LOG.debug("Recherche d'une tâche");
		final StringBuffer queryString = new StringBuffer();
		final Map<String, Object> parameters = new HashMap<String, Object>();
		generateSimpleQuery(searchCriteria, EntityTask.class, queryString, parameters);

		if (descriptionIsNull)
			addANDClause(queryString, "o.description IS NULL");
		if (categoryIsNull)
			addANDClause(queryString, "o.category IS NULL");
		if (expectedEndDateIsNull)
			addANDClause(queryString, "o.expectedEndDate IS NULL");
		if (expectedStartDateIsNull)
			addANDClause(queryString, "o.expectedStartDate IS NULL");
		if(!assignedUsersIsNull)
			for (final EntityTodo todo : searchCriteria.getTodo())
				addANDClause(queryString, "'"+todo+"' MEMBER OF o.todo");
		else
			addANDClause(queryString, "o.todo IS EMPTY");

		LOG.debug(queryString);
		final Query query = entityManager4TASK.createQuery(queryString.toString());
		for (final String key : parameters.keySet())
			query.setParameter(key, parameters.get(key));
		final List<EntityTask> tasks = query.getResultList();
		LOG.debug("Nombre d'objet en retour : " + tasks.size());
		return tasks;
	}

	private void addANDClause(final StringBuffer queryString, final String clause) {
		if (!queryString.toString().contains("WHERE"))
			queryString.append(" WHERE ");
		else
			queryString.append(" AND ");
		queryString.append(clause);
	}

	private void addORClause(final StringBuffer queryString, final String clause) {
		if (!queryString.toString().contains("WHERE"))
			queryString.append(" WHERE ");
		else
			queryString.append(" OR ");
		queryString.append(clause);
	}

	/**
	 * Génére une {@link javax.persistance.Query} en fonction de critére de
	 * recherche. Elle est générée par rapport à l'objet passé en paramètre.<br/>
	 * Les attributs de type collection ne peuvent générer des restrictions.
	 * 
	 * @param searchCriteria
	 *            l'entityBean contenant les critères de recherche.
	 * @param entityBeanClass
	 *            La classe de l'entity bean a traiter.
	 */
	private Query getSimpleSelectQuery(final Object searchCriteria, final Class entityBeanClass) {
		// TODO improve pouvoir forcer à charger les collection (fetch join)
		final StringBuffer queryString = new StringBuffer();
		final Map<String, Object> parameters = new HashMap<String, Object>();
		generateSimpleQuery(searchCriteria, entityBeanClass, queryString, parameters);
		final Query query = entityManager4TASK.createQuery(queryString.toString());
		for (final String key : parameters.keySet())
			query.setParameter(key, parameters.get(key));
		return query;
	}

	/*
	 * @param searchCriteria
	 *            l'entityBean contenant les critères de recherche.
	 * @param entityBeanClass
	 *            La classe de l'entity bean a traiter.
	 * @param StringBuffer
	 * 			Buffer dans lequel la requête sera générée.
	 * @param parameters
	 * 			Map dans laquelle seront placé les paramètres extrés de searchCriteria.
	 */
	private void generateSimpleQuery(final Object searchCriteria, final Class entityBeanClass, final StringBuffer queryString,
			final Map<String, Object> parameters) {
		try {
			final Map properties = BeanUtils.describe(searchCriteria);
			for (final Object key : properties.keySet())
				if (!key.toString().equals("class") && (properties.get(key) != null) && !properties.get(key).equals("[]")
						&& !(properties.get(key) instanceof Collection))
					try {
						LOG.debug(key.toString() + " " + properties.get(key));
						Object value = PropertyUtils.getProperty(searchCriteria, key.toString());
						if (value instanceof Collection)
							LOG.debug(key.toString() + " " + properties.get(key) + " rejeté !");
						else {
							if (queryString.length() == 0)
								queryString.append(" WHERE ");
							else
								queryString.append(" AND ");
							queryString.append("o." + key.toString() + "=:" + key.toString());

							if (value instanceof STMEntity) {
								LOG.debug("STMEntity : " + value.getClass().getName());
								value = find((STMEntity) value);
								LOG.debug("Id de l'entity = " + ((STMEntity) value).getUid());
							}
							parameters.put(key.toString(), value);
						}
					} catch (final Exception e) {
						LOG.info("Un des paramètres a provoqué une erreur. Le paramètre est ignoré.", e);
					}
		} catch (final Exception e) {
			LOG.error("Impossible de récupérer les critéres de la requête", e);
			throw new IllegalStateException("Impossible de récupérer les critéres de la requête");
		}
		final String className = entityBeanClass.getName().indexOf('.') > 0 ? entityBeanClass.getName().substring(
				entityBeanClass.getName().lastIndexOf('.') + 1) : entityBeanClass.getName();
				queryString.insert(0, "SELECT o from " + className + " o");
				LOG.debug("Requete générée: " + queryString.toString());
	}

	/**
	 * Renvoi la liste des commentaires, {@link EntityComment}, associés à une
	 * tâche, {@link EntityTask}.
	 * 
	 * @param task
	 *            la tâche pour laquelle récupérer les commentaires (
	 *            {@link EntityComment}).
	 * @return une liste vide dans le cas ou aucun resultat n'est trouvé.
	 */
	@SuppressWarnings("unchecked")
	public List<EntityComment> getComments(final EntityTask task) {
		if (LOG.isDebugEnabled())
			LOG.debug("Récupération de tous les commentaires pour la tâche " + task.getUid());
		return entityManager4TASK.createQuery("SELECT c FROM EntityComment c where c.task = ? order by c.id DESC").setParameter(1, task)
		.getResultList();
	}

	/**
	 * Renvoie la liste des tâches afféctées aux utilisateurs
	 * 
	 * @param userUids
	 *            liste des uid des users
	 * @return la liste des tâches afféctées aux utilisateurs, une liste vide si
	 *         aucun résultat n'est trouvé.
	 */
	public List<EntityTask> findTasksByUsers(final List<String> userUids) {
		//TODO improve
		final StringBuffer query = new StringBuffer();
		for (final String string : userUids) {
			if (query.length() > 0)
				query.append(',');
			query.append('\'');
			query.append(string);
			query.append('\'');
		}
		query.insert(0, "select DISTINCT task.* from task inner join todo on task.uid=todo.taskUid where todo.userUID IN (");
		query.append(")");
		LOG.debug(query);
		return entityManager4TASK.createNativeQuery(query.toString(), EntityTask.class).getResultList();
		//return tasks;
	}

	/**
	 * @param uid
	 * @return la tâche avec l'uid correspondant
	 */
	public EntityTask findTaskByUid(final String uid) {
		final EntityTask task = new EntityTask();
		task.setUid(uid);
		return (EntityTask) find(task);
	}

	public EntityComment findCommentByUid(final String uid) {
		final EntityComment comment = new EntityComment();
		comment.setUid(uid);
		return (EntityComment) find(comment);
	}

	public List<EntityTask> getUnaffectedTask() {
		final Query query = entityManager4TASK.createQuery("SELECT t from EntityTask t where t.users IS EMPTY");
		return query.getResultList();

	}

}
